/* Sentinel - IRC Statistical and Operator Services
** channels.c - Functions to track channel state
**
** Copyright W. Campbell and others.  See README for more details
** Some code Copyright: Jonathan George, Kai Seidler, ircd-hybrid Team,
**                      IRCnet IRCD developers.
**
** $Id: channels.c,v 1.59 2004/05/19 17:36:16 wcampbel Exp $
*/

#include <assert.h>
#include "stats.h"
#include "struct.h"
#include "proto.h"
#include "match.h"
#include "slog.h"
#include "mem.h"

#define MTYPE_NUL	0
#define MTYPE_ADD	1
#define MTYPE_DEL	2

static struct Channel *new_channel(char *);
struct Channel *chanlist[C_TABLE_SIZE];

static void add_channel_to_hash_table(char *, struct Channel *);
static void del_channel_from_hash_table(struct Channel *);

struct CMode
{
  char mode;
  unsigned int value;
};

static struct CMode mode_list[] =
  {
    { 'i', CMODE_INVITE },
    { 'm', CMODE_MOD },
    { 'n', CMODE_NOEXT },
    { 'p', CMODE_PRIV },
    { 's', CMODE_SEC },
    { 't', CMODE_TOPIC },
    { 'O', CMODE_OPERONLY },
    { 'r', CMODE_REGISTERED },
    { 'R', CMODE_REGONLY },
    { 'c', CMODE_NOCOLOUR },
    { 'j', CMODE_JUPED },
    { '\0', 0L }
  };

static struct CMode ignore_mode_list[] =
  {
    { 'b', CMODE_BAN },
    { 'e', CMODE_EXEMPT },
    { 'I', CMODE_INVEX },
    { '\0', 0L }
  };

static struct CMode status_mode_list[] =
  {
    { 'o', CMODE_OP },
    { 'v', CMODE_VOICE },
    { 'h', CMODE_HALFOP },
    { '\0', 0L }
  };

static void add_channel_to_hash_table(char *name, struct Channel *c)
{
  c->hash = HASH((unsigned char *) name, C_TABLE_SIZE);
  c->next = chanlist[c->hash];
  chanlist[c->hash] = c;
  debug("add_channel_to_hash_table: %s, %d", name, c->hash);
}

static void del_channel_from_hash_table(struct Channel *c)
{
  struct Channel *tmp, *prev = NULL;

  for (tmp = chanlist[c->hash]; tmp; tmp = tmp->next)
  {
    if (tmp == c)
    {
      if (prev)
        prev->next = tmp->next;
      else
        chanlist[c->hash] = tmp->next;
      tmp->next = NULL;
      return ;
    }
    prev = tmp;
  }
}

static struct Channel *new_channel(char *name)
{
  struct Channel *c;

  c = smalloc(sizeof(struct Channel));
  memcounts.channel++;
  if (!name)
    name = "";
  strlcpy(c->name, name, CHANLEN);
  add_channel_to_hash_table(name, c);

  debug("new_channel: %s", name);

  return c;
}

struct Channel *AddChannel(char *name, long ts)
{
  struct Channel *c;

  c = findchannel(name);
  if (c != NULL)
  {
    slog(DEFAULT_LOG, L_CRIT, "Channel %s already exists in the channel table",
         name);
    return c;
  }
  else
  {
    c = new_channel(name);
  }
  c->ts = ts;

  IncreaseChannels();

  return c;
}

void DelChannel(struct Channel *c)
{
  if (c == NULL)
  {
    slog(DEFAULT_LOG, L_CRIT, "DelChannel(): %s failed!", (c == NULL) ? "NULL" :
         c->name);
    return ;
  }

  del_channel_from_hash_table(c);
  DecreaseChannels();

  assert(netinfo.channels >= 0);
  assert(c->members.head == NULL);
  assert(c->members.tail == NULL);
  memcounts.channel--;
#ifdef LISTSERV
  if (c->topic)
  {
    memcounts.topic--;
    memcounts.topicmem -= strlen(c->topic) + 1;
    free(c->topic);
  }
  if (c->topicauthor)
  {
    memcounts.topicinfo--;
    memcounts.topicinfomem -= strlen(c->topicauthor) + 1;
    free(c->topicauthor);
  }
#endif
  free(c);
}

struct Channel *findchannel(char *name)
{
  struct Channel *c;

  debug("findchannel(%s)", name);
  c = chanlist[HASH((unsigned char *) name, C_TABLE_SIZE)];
  while (c && (irccasecmp(c->name, name) != 0))
    c = c->next;
  if (c)
    return c;

  return NULL;
}

void init_channel_hash()
{
  int i;
  struct Channel *c, *b;

  for (i = 0; i < C_TABLE_SIZE; i++)
  {
    c = chanlist[i];
    while (c != NULL)
    {
      b = c->next;
      debug("init_channel_hash() deleting %s", c->name);
      memcounts.channel--;
      free(c);
      c = b;
    }
    chanlist[i] = NULL;
  }
}

void IncreaseChannels()
{
  debug("Increasing channel count");
  netinfo.channels++;
  if (netinfo.channels > netinfo.maxchannels)
  {
    netinfo.maxchannels = netinfo.channels;
    netinfo.t_maxchannels = CurrentTime;
  }
  if (netinfo.channels > daily.channels)
  {
    daily.channels = netinfo.channels;
    daily.t_channels = CurrentTime;
  }
}

#ifdef IRCD_BAHAMUT
void ChannelMode(struct Channel *c, char *tsstr, int parc, char *parv[])
#else
void ChannelMode(struct Channel *c, int parc, char *parv[])
#endif
{
  int i;
  char *pos = parv[0];
  int matched;
  int parpos = 1;
  int whatt = MTYPE_NUL;
  struct ChanUser *cu = NULL;

  if ((pos == NULL) || (*pos == '\0'))
    return ;

  if (c == NULL)
    return ;

  /* SJOIN modes of 0 mean no change */
  if (*pos == '0')
    return ;

#ifdef IRCD_BAHAMUT
  do
  {
    long ts;

    ts = atol(tsstr);
    if ((ts != 0) && (ts > c->ts))
      return ;
  } while(FALSE);
#endif

#ifdef LISTSERV
  /* a mode is activity */
  c->last = CurrentTime;
#endif

  /* assumed that the parc is correct, bad but the servers do the same */
  for (; *pos != '\0'; pos++)
  {
    matched = FALSE;
    if (*pos == '+')
    {
      whatt = MTYPE_ADD;
      continue;
    }
    if (*pos == '-')
    {
      whatt = MTYPE_DEL;
      continue;
    }

    for (i = 0; mode_list[i].mode != '\0'; i++)
    {
      if (*pos == mode_list[i].mode)
      {
        matched = TRUE;
        if (whatt == MTYPE_ADD)
          c->modes |= mode_list[i].value;
        else
          c->modes &= ~mode_list[i].value;
        break;
      }
    }
    if (matched)
      continue;
    for (i = 0; ignore_mode_list[i].mode != '\0'; i++)
    {
      if (*pos == ignore_mode_list[i].mode)
      {
        matched = TRUE;
        parpos++;
        break;
      }
    }
    if (matched)
      continue;
    if (*pos == 'l')
    {
      if (whatt == MTYPE_ADD)
      {
        c->modes |= CMODE_LIMIT;
        c->limit = atoi(parv[parpos]);
        parpos++;
      }
      else
      {
        c->modes &= ~CMODE_LIMIT;
        c->limit = 0;
      }
      continue;
    }
    if (*pos == 'k')
    {
      if (whatt == MTYPE_ADD)
      {
        c->modes |= CMODE_KEY;
        strlcpy(c->key, parv[parpos], KEYLEN);
        parpos++;
      }
      else
      {
        c->modes &= ~CMODE_KEY;
        c->key[0] = '\0';
        /* Hybrid typically sends either the key or
        ** * on -k, so you should eat a parameter
        */
        parpos++;
      }
      continue;
    }
    for (i = 0; status_mode_list[i].mode != '\0'; i++)
    {
      if (*pos == status_mode_list[i].mode)
      {
        cu = findchanuser(c, finduser(parv[parpos]));
        if (cu == NULL)
        {
          slog(DEFAULT_LOG, L_ERR, "CHANNEL ERROR:  MODE %s "
               "%c%c %s", c->name,
               (whatt == MTYPE_ADD) ? '+' :
               '-', status_mode_list[i].value,
               parv[parpos]);
          parpos++;
          matched = TRUE;
        }
        if (matched == TRUE)
          break;
        matched = TRUE;
        if (whatt == MTYPE_ADD)
        {
          cu->modes |= status_mode_list[i].value;
        }
        else
        {
          cu->modes &= ~status_mode_list[i].value;
        }
        parpos++;
        break;
      }
    }
    if (matched)
      continue;
    debug("ChanMode() mode %c not matched", *pos);
  }
}

char *ModeString(struct Channel *c)
{
  static char modebuf[LINE];
  char limbuf[LIMITLEN];
  char *ptr = modebuf;
  int i;

  *ptr = '+';
  ptr++;

  for (i = 0; mode_list[i].mode != '\0'; i++)
  {
    if (c->modes & mode_list[i].value)
    {
      *ptr = mode_list[i].mode;
      ptr++;
    }
  }
  if (c->modes & CMODE_KEY)
  {
    *ptr = 'k';
    ptr++;
  }
  if (c->modes & CMODE_LIMIT)
  {
    *ptr = 'l';
    ptr++;
  }

  *ptr = '\0';

  if (c->modes & CMODE_KEY)
  {
    strlcat(modebuf, " ", LINE);
    strlcat(modebuf, c->key, LINE);
  }
  if (c->modes & CMODE_LIMIT)
  {
    snprintf(limbuf, LIMITLEN, "%d", c->limit);
    strlcat(modebuf, " ", LINE);
    strlcat(modebuf, limbuf, LINE);
  }

  return modebuf;
}

char *CUModeString(struct ChanUser *cu)
{
  /* increase the size if more modes are used */
  static char modebuf[4];
  char *modeptr = modebuf;

  if (cu->modes & CMODE_OP)
  {
    *modeptr = '@';
    modeptr++;
  }
  if (cu->modes & CMODE_HALFOP)
  {
    *modeptr = '%';
    modeptr++;
  }
  if (cu->modes & CMODE_VOICE)
  {
    *modeptr = '+';
    modeptr++;
  }
  *modeptr = '\0';
  return modebuf;
}

void AddUserToChannel(struct Channel *c, char *userptr)
{
  dlink_node *dl1;
  dlink_node *dl2;
  struct ChanUser *tcu;
  struct ChanUser *cu;
  unsigned int modifiers = 0;
  struct User *u;

  while (*userptr != '\0')
  {
    if (*userptr == '@')
    {
      modifiers |= CMODE_OP;
      userptr++;
      continue;
    }
    if (*userptr == '+')
    {
      modifiers |= CMODE_VOICE;
      userptr++;
      continue;
    }
    if (*userptr == '%')
    {
      modifiers |= CMODE_HALFOP;
      userptr++;
      continue;
    }
    /* If I get here, I've parsed everything and it SHOULD be
    ** the nick.
    */
    break;
  }

  u = finduser(userptr);

  if (u == NULL)
  {
    slog(DEFAULT_LOG, L_WARNING, "AddUserToChannel: Trying to add unknown user %s to %s",
         userptr, c->name);
    return ;
  }

  tcu = findchanuser(c, u);
  if (tcu != NULL)
  {
    slog(DEFAULT_LOG, L_WARNING, "AddUserToChannel: %s already in %s", userptr,
         c->name);
    /* Since this could be from an OPME, still set the modifiers */
    tcu->modes |= modifiers;
    return ;
  }

  dl1 = dlink_create();
  dl2 = dlink_create();
  cu = smalloc(sizeof(struct ChanUser));
  memcounts.chanuser++;

  cu->chan = c;
  cu->user = u;
  cu->modes |= modifiers;

  dlink_add(cu, dl1, &c->members);
  dlink_add(cu, dl2, &u->channels);
  c->nummembers++;

#ifdef LISTSERV
  c->last = CurrentTime;
#endif

#if defined(MONSERV) && defined(MON_DRONES)

  CheckChannelDrones(u, c);
#endif
}

void AddUserFlagToChannel(struct Channel *c, struct User *u, unsigned int flags)
{
  dlink_node *dl1;
  dlink_node *dl2;
  struct ChanUser *tcu;
  struct ChanUser *cu;

  tcu = findchanuser(c, u);
  if (tcu != NULL)
  {
    slog(DEFAULT_LOG, L_WARNING, "AddUserFlagToChannel: %s already in %s",
         u->nick, c->name);
    /* Since this could be from an OPME, still set the modifiers */
    tcu->modes |= flags;
    return ;
  }

  dl1 = dlink_create();
  dl2 = dlink_create();
  cu = smalloc(sizeof(struct ChanUser));
  memcounts.chanuser++;

  cu->chan = c;
  cu->user = u;
  cu->modes |= flags;

  dlink_add(cu, dl1, &c->members);
  dlink_add(cu, dl2, &u->channels);
  c->nummembers++;

#ifdef LISTSERV
  c->last = CurrentTime;
#endif

#if defined(MONSERV) && defined(MON_DRONES)

  CheckChannelDrones(u, c);
#endif
}

/* Side effect, free's the ChanUser struct */
void DelUserFromChannel(struct Channel *c, struct User *u)
{
  dlink_node *dl;
  struct ChanUser *cu;

  DLINK_FOREACH(dl, c->members.head)
  {
    cu = (struct ChanUser *) dl->data;
    if (cu->user == u)
    {
      dlink_delete(dl, &c->members);
      free(cu);
      memcounts.chanuser--;
      dlink_free(dl);
      c->nummembers--;
      if (c->nummembers == 0)
      {
        DelChannel(c);
      }
#ifdef LISTSERV
      else
      {
        c->last = CurrentTime;
      }
#endif
      return ;
    }
  }
  slog(DEFAULT_LOG, L_CRIT, "DelUserFromChannel(%s, %s) failed", c->name,
       u->nick);
}

void UnlinkChannelFromUser(struct Channel *c, struct User *u)
{
  dlink_node *dl;
  struct ChanUser *cu;

  DLINK_FOREACH(dl, u->channels.head)
  {
    cu = (struct ChanUser *) dl->data;
    if (cu->chan == c)
    {
      dlink_delete(dl, &u->channels);
      dlink_free(dl);
      return ;
    }
  }
  slog(DEFAULT_LOG, L_CRIT, "UnlinkChannelFromUser(%s, %s) failed", c->name,
       u->nick);
}

struct ChanUser *findchanuser(struct Channel *c, struct User *u)
{
  dlink_node *dl;
  struct ChanUser *cu;

  if ((c == NULL) || (u == NULL))
    return NULL;

  DLINK_FOREACH(dl, c->members.head)
  {
    cu = (struct ChanUser *) dl->data;
    if (cu->user == u)
      return cu;
  }

  return NULL;
}

void HandleTSChange(struct Channel *c, time_t ts)
{
  dlink_node *dl;
  struct ChanUser *cu;

  /* TS change requires the following things to be done to the
  ** channel:  reset all modes to nothing, remove all status modes
  ** on known users on the channel (including our own), and set the
  ** new TS.
  */

  c->modes = 0;
  c->limit = 0;
  c->key[0] = '\0';

  DLINK_FOREACH(dl, c->members.head)
  {
    cu = dl->data;
    cu->modes = 0;

    /* XXX - CPU intensive but maybe check if it's one of
    ** our services bots here?
    */
  }

  c->ts = ts;
}

